/**
 * Copyright 2011 Google Inc.
 * Copyright 2011 Joe LaPenna
 *
 * Main javascript for a checkins mashup.
 */

goog.provide('checkintrack.map')

goog.require('checkintrack.ui.map');

goog.require('goog.Timer');
goog.require('goog.Uri');
goog.require('goog.date.DateTime');
goog.require('goog.dom.ViewportSizeMonitor');
goog.require('goog.net.XhrIo');
goog.require('goog.structs.Queue');

goog.require('soy');


/**
 * Controls rendering and data querying for the checkins map.
 * @param {goog.net.Uri} uri the url the javascript is loading from.
 * @constructor
 */
checkintrack.map.Controller = function(uri) {

  /**
   * The map to render upon.
   * @private
   */
  this.map = new google.maps.Map(document.getElementById("map"), {
    zoom: 4,
    center: new google.maps.LatLng(37.44, -122.14),
    mapTypeId: google.maps.MapTypeId.ROADMAP
  });

  /**
   * The checkins section to show checkin details.
   * @private
   */
  this.checkinsSection = document.getElementById("checkinsSection");

  /**
   * A window to display info about a checkin.
   * @private
   */
  this.infoWindow = new google.maps.InfoWindow();

  /**
   * Monitors resize events so we can resize the map.
   * @private
   */
  this.vsm = new goog.dom.ViewportSizeMonitor();
  goog.events.listen(this.vsm, goog.events.EventType.RESIZE, goog.bind(function(e) {
    google.maps.event.trigger(this.map, 'resize');
  }), this);

  /**
   * Refresh Timer
   * @private
   */
  this.refreshTimer = new goog.Timer(checkintrack.map.Controller.REFRESH_TIMER_MS);

  /**
   * Rendering Timer
   * @private
   */
  this.panningTimer = new goog.Timer(checkintrack.map.Controller.REFRESH_TIMER_MS
                                     / checkintrack.map.Controller.UPDATES_PER_REFRESH);

  /**
   * Rendered checkins.
   * @private
   */
  this.checkins = new goog.structs.Queue();

  /**
   * Array of checkins waiting to be rendered.
   */
  this.queue = new goog.structs.Queue();

  /**
   * Most recent checkin from checkins.
   */
  this.lastCheckin = null;

  /**
   * The oldest a rendered checkin can be.
   */
  this.oldestSec = (new goog.date.DateTime().getTime() / 1000)
      - checkintrack.map.Controller.BACKFILL_SEC;

  /**
   * The newest a checkin can be.
   */
  this.newestSec = new goog.date.DateTime().getTime() / 1000;

  /**
   * The last time checkins were rendered.
   */
  this.stepSizeSec = 0;
}

/**
 * The largest size of visible markers;
 * @type {number}
 */
checkintrack.map.Controller.MAX_MARKERS = 1000;

/**
 * The number of millisecnods between timer ticks.
 * @type {number}
 */
checkintrack.map.Controller.REFRESH_TIMER_MS = 5000;

/**
 * The number of millisecnods between timer ticks.
 * @type {number}
 */
checkintrack.map.Controller.UPDATES_PER_REFRESH = 5;

/**
 * The number of seconds before now to back-fill rendering.
 * @type {number}
 */
checkintrack.map.Controller.BACKFILL_SEC = 300;

/**
 * The maximum age of a checkin.
 * @type {number}
 */
checkintrack.map.Controller.MAX_AGE_SEC = 300;

/**
 * The path of the checkin stream RPC
 * @type {string}
 */
checkintrack.map.Controller.STREAM_PATH = "/checkins";

/**
 * The icon to show for markers
 * @type {string}
 */
checkintrack.map.Controller.ICON = "/static/fresh_checkin.png";

/**
 * Start data querying for display on the map.
 */
checkintrack.map.Controller.prototype.start = function() {
  // Fire the first request.
  this.onRefreshTick();

  // Bind ticks to the refresh timer.
  goog.events.listen(this.refreshTimer, goog.Timer.TICK,
      goog.bind(this.onRefreshTick, this));
  this.refreshTimer.start();

  // Bind ticks to the panning timer.
  goog.events.listen(this.panningTimer, goog.Timer.TICK,
      goog.bind(this.onRenderingTick, this));
  this.panningTimer.start();
}

/**
 * Fired whenever the timer ticks.
 * @private
 */
checkintrack.map.Controller.prototype.onRefreshTick = function() {
  console.log('checkintrack.map.Controller.onRefreshTick');
  goog.net.XhrIo.send(checkintrack.map.Controller.STREAM_PATH,
      goog.bind(this.onCheckinStream, this));
}

/**
 * Fired whenever an updated checkin stream is returned.
 * @param e {Event} the event fired after hitting the checkins stream.
 * @private
 */
checkintrack.map.Controller.prototype.onCheckinStream = function(e) {
  console.log('checkintrack.map.Controller.onCheckinStream');
  var xhr = e.target;
  var latestCheckins = xhr.getResponseJson();
  this.newestSec = (new goog.date.DateTime().getTime()
                         + checkintrack.map.Controller.REFRESH_TIMER_MS) / 1000;
  this.stepSizeSec = (this.newestSec - this.oldestSec)
                      / checkintrack.map.Controller.UPDATES_PER_REFRESH;


  // Find the oldest checkin in the latest set of checkins we don't already
  // have.
  var queueTail = latestCheckins.length;
  for (var i = 0; i < latestCheckins.length; i++) {
    if (this.lastCheckin != null) {
      if (this.lastCheckin.link == latestCheckins[i].link) {
        queueTail = i;
        break;
      }
    }
  }
  console.log('Queue tail: ', queueTail);

  // Queue up new things to render.
  if (queueTail > 0) {
    var toQueue = latestCheckins.slice(0, queueTail);
    for (var i = toQueue.length - 1; i >= 0; i--) {
      this.queue.enqueue(toQueue[i]);
    }
    // Track the freshest checkin for next time.
    this.lastCheckin = toQueue[0];

    // Debug logging
    console.log('Queue size: ', this.queue.getCount());
    console.log('Queue range: ', toQueue[0].published, toQueue[queueTail - 1].published);
    console.log('Step size: ', this.stepSizeSec);
  }
}

/**
 * Fired whenever the panning timer ticks.
 * @private
 */
checkintrack.map.Controller.prototype.onRenderingTick = function() {
  console.log('checkintrack.map.Controller.onRenderingTick');

  // We'll be rendering the next segment.
  this.newestSec = this.newestSec + this.stepSizeSec;
  console.log('Rendering Range: ', this.oldestSec, this.newestSec);

  // The oldest a checkin marker can be.
  var oldestPublishTime = this.oldestSec - checkintrack.map.Controller.MAX_AGE_SEC;

  var latestMarker = null;
  var latestCheckin = null;
  while (!this.queue.isEmpty()) {
    var queuedPublishTime = this.queue.peek().published;
    if (queuedPublishTime < this.oldestSec) {
      // Checkin is too old, don't render it.
      console.log('Checkin too old ($1), expected > $2', queuedPublishTime, this.oldestSec);
      this.queue.dequeue();
      continue;
    } else if (queuedPublishTime > this.newestSec) {
      console.log('Checkin too new ($1), expected < $2', queuedPublishTime, this.newestSec);
      // Checkin is too new, don't render it yet.
      break;
    }

    // Remove old checkins to make room for new ones.
    while (this.checkins.getCount() >= checkintrack.map.Controller.MAX_MARKERS
           || (this.checkins.peek() != null
               && this.checkins.peek().published < oldestPublishTime)) {
      var oldestCheckinMarker = this.checkins.dequeue();
      this.removeCheckin(oldestCheckinMarker);
    }

    // Add a new checkin.
    var latestCheckin = this.queue.dequeue();
    var latestMarker = this.addCheckin(latestCheckin);

    // We've published anything older than this already, shrink the range.
    this.oldestSec = latestCheckin.published;
  }

  this.focusCheckin(latestCheckin, latestMarker);
}

checkintrack.map.Controller.prototype.focusCheckin = function(checkin, marker) {

  if (marker != null && checkin != null) {
    // Focus on the newest checkin.
    this.map.panTo(marker.getPosition());

    var published = new goog.date.DateTime();
    published.setTime(checkin.published * 1000);

    // Show its info window.
    this.infoWindow.setContent(checkintrack.ui.map.checkin({
      link: checkin.link,
      featureName: checkin.feature_name,
      addressFormatted: checkin.address_formatted,
      authorUrl: checkin.author_url,
      author: checkin.author,
      published: published.toUsTimeString()
    }));
    this.infoWindow.open(this.map, marker);
  }
}

checkintrack.map.Controller.prototype.removeCheckin = function(checkinMarker) {
  // Remove it from the map.
  checkinMarker.setMap(null);

  // Remove it from the list of checkins.
  goog.dom.removeNode(goog.dom.getLastElementChild(this.checkinsSection));
}

checkintrack.map.Controller.prototype.addCheckin = function(checkin) {
  // Add it to the map.
  var latLng = new google.maps.LatLng(checkin.point[0], checkin.point[1]);
  var title = (checkin.feature_name != null)
      ? checkin.feature_name : checkin.address_formatted;

  var latestMarker = new google.maps.Marker({
      map: this.map,
      position: latLng,
      title: title,
      icon: checkintrack.map.Controller.ICON
  })

  var published = new goog.date.DateTime();
  published.setTime(checkin.published * 1000);

  // Add it to the list of checkins
  var checkinDiv = soy.renderAsFragment(checkintrack.ui.map.checkin, {
      link: checkin.link,
      featureName: checkin.feature_name,
      addressFormatted: checkin.address_formatted,
      authorUrl: checkin.author_url,
      author: checkin.author,
      published: published.toUsTimeString()
    });
  var firstChild = goog.dom.getFirstElementChild(this.checkinsSection);
  if (firstChild == null) {
    goog.dom.append(this.checkinsSection, checkinDiv);
  } else {
    goog.dom.insertSiblingBefore(checkinDiv, firstChild);
  }

  // Keep track of the marker.
  this.checkins.enqueue(latestMarker);

  return latestMarker;
}


/**
 * Entry point for /map
 */
checkintrack.map.init = function() {
  var uri = new goog.Uri(window.location);
  var controller = new checkintrack.map.Controller(uri);
  controller.start();
}
goog.exportSymbol('checkintrack.map.init', checkintrack.map.init);
